小心使用 method swizzling

使用 method swizzling,我们通常会想到一个方法:void method_exchangeImplementations(Method m1, Method m2)下面我们测一下这个方法。

首先创建一个简单的工程,根控制器是一个 UINavigationController navi,navi的根控制器是一个ViewController vc,而ViewController继承自BaseViewController,在BaseViewController里只有一个方法:

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
}

然后我们创建一个BaseViewController的分类BaseViewController+Swizzling,这个分类里有如下两个方法:

+ (void)load
{
    Method originalMethod = class_getInstanceMethod(self, @selector(viewWillAppear:));
    Method swizzledMethod = class_getInstanceMethod(self, @selector(ext_viewWillAppear:));
    method_exchangeImplementations(originalMethod, swizzledMethod);
}

- (void)ext_viewWillAppear:(BOOL)animated
{
    NSLog(@"self: %@, %s", self, __func__);
    [self ext_viewWillAppear:animated];
}

运行工程,crash日志如下:

2019-03-28 11:00:07.205536+0800 TestMethodSwizzling[40022:14367088] self: <UINavigationController: 0x7fefdf01dc00>, -[BaseViewController(Swizzling) ext_viewWillAppear:]
2019-03-28 11:00:21.830011+0800 TestMethodSwizzling[40022:14367088] -[UINavigationController ext_viewWillAppear:]: unrecognized selector sent to instance 0x7fefdf01dc00
2019-03-28 11:00:21.838515+0800 TestMethodSwizzling[40022:14367088] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[UINavigationController ext_viewWillAppear:]: unrecognized selector sent to instance 0x7fefdf01dc00'

这个crash让人很困惑,因为我们发现,navi 在尝试执行ext_viewWillAppear:方法时发生了崩溃。现在让我们看看到底发生了什么。

我们知道,程序在开始运行之后,navi 会调用自己的viewWillAppear:方法,先不用管UINavigationController有没有 override UIViewControllerviewWillAppear:方法,这无关紧要,因为最终 navi 还是要调用它的父类,也就是 UIViewControllerviewWillAppear:,这没有问题。但是,UIViewControllerviewWillAppear:方法已经不是最初的实现了,因为在BaseViewController+Swizzlingload方法被调用之后,它的实现已经变成了:

    NSLog(@"self: %@, %s", self, __func__);
    [self ext_viewWillAppear:animated];

很多人又困惑了,我交换的是BaseViewControllerviewWillAppear:,跟UIViewControllerviewWillAppear:有什么关系?有关系!因为现在BaseViewController并没有 override viewWillAppear:,也就是说,BaseViewController自己并没有viewWillAppear:方法,它用的是它的父类,也就是 UIViewControllerviewWillAppear:方法。所以你感觉你交换的是BaseViewController自己的viewWillAppear:方法,但是却无意中改变了 UIViewControllerviewWillAppear:的方法实现,然后代码执行到[self ext_viewWillAppear:animated];,控制器发现自己并没有ext_viewWillAppear:方法,于是就崩溃了。

这个影响是非常大的,因为你项目中所有的控制器都继承自 UIViewController (包括UINavigationController),任何一个控制器直接或间接调用 UIViewControllerviewWillAppear:都将引起崩溃。现在已经找到崩溃原因了,我们很容易想到一个解决方案:在BaseViewController中添加viewWillAppear:方法:

- (void)viewWillAppear:(BOOL)animated
{
    [super viewWillAppear:animated];
}

运行,OK!但是,这个方案并不保险。如果在一个已经经过了很多人维护的项目中,突然有一天有人发现这块代码什么都没做,删了吧,一运行,crash。找了半天原因,发现是你加了这个自以为是的 method swizzling。事实上,这块代码除了解决 method swizzling 带来的crash问题,的确什么都没做。而实际情况要复杂甚至严重的多,因为有时候可能因为删了某块貌似冗余的代码,测试也OK,但是一上线却经常出现一些莫名其妙的问题。

这就是runtime,你可以任意访问一些私有方法和属性,也可以动态添加、修改甚至删除一些方法和属性,但是如果使用不善,很多问题可能不能在开发和测试阶段立即暴露出来,但是一上线就是一个随时可能爆炸的炸弹。所以,如果你不能很好的理解和掌握runtime,那就尽量不要使用这些奇技淫巧,而是要坚持软件开发的一些基本原则和模式,这才是阳光大道。知道自己该干什么,不该干什么,知道一个类、一个方法该干什么,不该干什么,这是一个哲学问题。

废话不多说,我们说下一个解决方案。既然不能依赖BaseViewControllerviewWillAppear:方法,那我们就要从自己身上找问题,而不是寄希望于别人。现在删掉BaseViewControllerviewWillAppear:方法,然后把BaseViewController+Swizzling中的load方法修改如下:

+ (void)load
{
    SEL originalSel = @selector(viewWillAppear:);
    Method originalMethod = class_getInstanceMethod(self, originalSel);
    IMP originalImp = method_getImplementation(originalMethod);

    SEL swizzledSel = @selector(ext_viewWillAppear:);
    Method swizzledMethod = class_getInstanceMethod(self, swizzledSel);
    IMP swizzledImp = method_getImplementation(swizzledMethod);

    if (class_addMethod(self, originalSel, swizzledImp, method_getTypeEncoding(swizzledMethod))) {
        class_replaceMethod(self, swizzledSel, originalImp, method_getTypeEncoding(originalMethod));
    }
    else {
        method_exchangeImplementations(originalMethod, swizzledMethod);
    }
}

运行,OK!这段代码的意思很明白。class_addMethod(self, originalSel, swizzledImp, method_getTypeEncoding(swizzledMethod))这个方法的意思是尝试给BaseViewController添加originalSel方法,且对应的实现是swizzledImp。如果BaseViewController自身已存在originalSel方法,则方法添加失败,且返回NO,这时候直接调用method_exchangeImplementations(originalMethod, swizzledMethod)进行 method swizzling 即可。如果BaseViewController自身不存在originalSel(可能父类存在该方法,但是这里的"自身不存在"表示不考虑父类),则返回YES,方法添加成功,这时候再调用class_replaceMethod(self, swizzledSel, originalImp, method_getTypeEncoding(originalMethod)),将BaseViewControllerswizzledSel的方法实现替换为originalImp,这样就实现了添加并交换方法实现的目的。

正常到这里就结束了,但这并不全面,让我们更进一步。

首先,让BaseViewController遵守如下协议,但并不实现协议里的方法。

@protocol TestMethodSwizzlingProtocol <NSObject>

@optional
- (void)doSomething;

@end

然后我们修改BaseViewController+Swizzling里的方法如下:

+ (void)load
{
    SEL originalSel = @selector(doSomething);
    Method originalMethod = class_getInstanceMethod(self, originalSel);
    IMP originalImp = method_getImplementation(originalMethod);

    SEL swizzledSel = @selector(ext_doSomething);
    Method swizzledMethod = class_getInstanceMethod(self, swizzledSel);
    IMP swizzledImp = method_getImplementation(swizzledMethod);

    if (class_addMethod(self, originalSel, swizzledImp, method_getTypeEncoding(swizzledMethod))) {
        class_replaceMethod(self, swizzledSel, originalImp, method_getTypeEncoding(originalMethod));
    }
    else {
        method_exchangeImplementations(originalMethod, swizzledMethod);
    }
}

- (void)ext_doSomething
{
    NSLog(@"self: %@, %s", self, __func__);
    [self ext_doSomething];
    NSLog(@"never executed");
}

这段代码其实就是对 doSomething进行 method swizzling。然后我们在 vc 中添加一个 button,点击 button 时调用 [self doSomething]。运行,点击button,发现程序执行到ext_doSomething发生了死循环,根本停不下来,NSLog(@"never executed")将永远不会执行。

分析原因发现,由于BaseViewController自身并没有实现doSomething方法,所以在BaseViewController+Swizzlingload方法中,originalMethodoriginalImp都为nil。class_addMethod(self, originalSel, swizzledImp, method_getTypeEncoding(swizzledMethod))成功执行,但是执行class_replaceMethod(self, swizzledSel, originalImp, method_getTypeEncoding(originalMethod))却不会有任何效果,因为originalImp为nil。也就是说我们只替换了@selector(doSomething)的方法实现,却没有改变@selector(ext_doSomething)对应的方法实现。换而言之,这个时候@selector(doSomething)@selector(ext_doSomething)都指向相同的实现:

    NSLog(@"self: %@, %s", self, __func__);
    [self ext_doSomething];
    NSLog(@"never executed");

有人说在 BaseViewController里实现doSomething方法不就行了。正如前面所说,这是一种方案,但不是一种好的方案。但是这个方案很有启发意义,我们可以在BaseViewController+Swizzling中加一个doSomething的默认实现placeholder_doSomething

- (void)placeholder_doSomething
{
    NSLog(@"I am BaseViewController's default implementaion of 'doSomething' method.");
}

然后修改load中的代码如下:

+ (void)load
{
    SEL originalSel = @selector(doSomething);
    Method originalMethod = class_getInstanceMethod(self, originalSel);
    IMP originalImp = method_getImplementation(originalMethod);

    SEL swizzledSel = @selector(ext_doSomething);
    Method swizzledMethod = class_getInstanceMethod(self, swizzledSel);
    IMP swizzledImp = method_getImplementation(swizzledMethod);

    // 判断原方法是否有对应实现
    if (!originalMethod || !originalImp) {

        SEL placeholderSel = @selector(placeholder_doSomething);
        Method placeholderMethod = class_getInstanceMethod(self, placeholderSel);
        IMP placeholderImp = method_getImplementation(placeholderMethod);

        BOOL succ = class_addMethod(self, originalSel, swizzledImp, method_getTypeEncoding(swizzledMethod));
        if (succ) {
            assert(placeholderMethod && placeholderImp);
            class_replaceMethod(self, swizzledSel, placeholderImp, method_getTypeEncoding(placeholderMethod));
        }
        else {
            NSAssert(0, @"something goes wrong!");
        }
        return;
    }

    if (class_addMethod(self, originalSel, swizzledImp, method_getTypeEncoding(swizzledMethod))) {
        class_replaceMethod(self, swizzledSel, originalImp, method_getTypeEncoding(originalMethod));
    }
    else {
        method_exchangeImplementations(originalMethod, swizzledMethod);
    }
}

也就是说,如果BaseViewController没有实现doSomething方法,@selector(ext_doSomething)将与@selector(placeholder_doSomething)进行方法交换;反之,@selector(ext_doSomething)将与@selector(doSomething)进行方法交换。

运行,点击button,当BaseViewController没有实现doSomething方法时,[self ext_doSomething]将执行placeholder_doSomething里的代码;反之,[self ext_doSomething]将执行doSomething里的代码。到此,问题解决。为了方便复用,我们从load的代码中抽出一个通用方法:

void swizzleInstanceMethod(Class theClass, SEL originalSel, SEL swizzledSel, SEL placeholderSel)
{
    Method originalMethod = class_getInstanceMethod(theClass, originalSel);
    IMP originalImp = method_getImplementation(originalMethod);

    Method swizzledMethod = class_getInstanceMethod(theClass, swizzledSel);
    IMP swizzledImp = method_getImplementation(swizzledMethod);

    Method placeholderMethod = class_getInstanceMethod(theClass, placeholderSel);
    IMP placeholderImp = method_getImplementation(placeholderMethod);

    // 判断原方法是否有对应实现
    if (!originalMethod || !originalImp) {
        BOOL succ = class_addMethod(theClass, originalSel, swizzledImp, method_getTypeEncoding(swizzledMethod));
        if (succ) {
            assert(placeholderMethod && placeholderImp);
            class_replaceMethod(theClass, swizzledSel, placeholderImp, method_getTypeEncoding(placeholderMethod));
        }
        else {
            NSLog(@"something goes wrong!");
            assert(0);
        }
        return;
    }

    if (class_addMethod(theClass, originalSel, swizzledImp, method_getTypeEncoding(swizzledMethod))) {
        class_replaceMethod(theClass, swizzledSel, originalImp, method_getTypeEncoding(originalMethod));
    }
    else {
        method_exchangeImplementations(originalMethod, swizzledMethod);
    }
}

修改load中的代码如下:

+ (void)load
{
    SEL originalSel = @selector(doSomething);
    SEL swizzledSel = @selector(ext_doSomething);
    SEL placeholderSel = @selector(placeholder_doSomething);
    swizzleInstanceMethod(self, originalSel, swizzledSel, placeholderSel);
}

至此,OK!

但是还没有结束,我们要再进一步!

假如UIViewController中有一个方法doNotSuperCallMe

- (void)doNotSuperCallMe
{
    NSLog(@"self: %@, %s", self, __func__);
    assert(0);
}

这个方法,子类可以 override,但是子类不能调用[super doNotSuperCallMe],否者将导致崩溃。现在我们的BaseViewController不确定未来是否要 override 这个方法,但是我们想要在BaseViewController+Swizzling中 hook 这个方法,代码如下:

+ (void)load
{
    SEL originalSel = @selector(doNotSuperCallMe);
    SEL swizzledSel = @selector(ext_doNotSuperCallMe);
    swizzleInstanceMethod(self, originalSel, swizzledSel, nil);
}

- (void)ext_doNotSuperCallMe
{
    NSLog(@"self: %@, %s", self, __func__);
    [self ext_doNotSuperCallMe];
}

然后修改 vc 中的 button 为点击时调用[self doNotSuperCallMe],并且此时的BaseViewControllerViewController中并没有override doNotSuperCallMe方法。好了,看起来没什么问题,运行,点击button,发现代码执行到了UIViewControllerdoNotSuperCallMe方法而发生了崩溃。这不是我们想要的结果,我们不想调用父类,也就是UIViewControllerdoNotSuperCallMe方法,因为刚才已经说过这个方法是禁止调用的,如果非要调用将导致崩溃。

通过分析发现,因为BaseViewController自身没有doNotSuperCallMe方法,所以 method swizzling 之后@selector(ext_doNotSuperCallMe)指向了父类doNotSuperCallMe的实现,所以调用[self ext_doNotSuperCallMe]将执行父类中的实现而导致崩溃。如果在BaseViewController中 override doNotSuperCallMe方法则程序运行正常。据此,我们想到了 placeholder 方案。所干就干,修改代码如下:

+ (void)load
{
    SEL originalSel = @selector(doNotSuperCallMe);
    SEL swizzledSel = @selector(ext_doNotSuperCallMe);
    SEL placeholderSel = @selector(placeholder_doNotSuperCallMe);
    swizzleInstanceMethod(self, originalSel, swizzledSel, placeholderSel);
}

- (void)ext_doNotSuperCallMe
{
    NSLog(@"self: %@, %s", self, __func__);
    [self ext_doNotSuperCallMe];
}

- (void)placeholder_doNotSuperCallMe
{
    NSLog(@"I am BaseViewController's default implementaion of 'doNotSuperCallMe' method, and I won't call [super doNotSuperCallMe]!");
}

运行,点击button,Crash Again!

继续分析原因,swizzleInstanceMethod这个函数还需要修改一下:

void swizzleInstanceMethod(Class theClass, SEL originalSel, SEL swizzledSel, SEL placeholderSel)
{
    Method originalMethod = class_getInstanceMethod(theClass, originalSel);
    IMP originalImp = method_getImplementation(originalMethod);

    Method swizzledMethod = class_getInstanceMethod(theClass, swizzledSel);
    IMP swizzledImp = method_getImplementation(swizzledMethod);

    Method placeholderMethod = class_getInstanceMethod(theClass, placeholderSel);
    IMP placeholderImp = method_getImplementation(placeholderMethod);

    // 判断原方法是否有对应实现
    if (!originalMethod || !originalImp) {
        BOOL succ = class_addMethod(theClass, originalSel, swizzledImp, method_getTypeEncoding(swizzledMethod));
        if (succ) {
            // 此时(theClass 本身和它所有父类都没有 originalSel 的方法实现),
            // 必须提供 placeholder 用于方法交换
            assert(placeholderMethod && placeholderImp);
            class_replaceMethod(theClass, swizzledSel, placeholderImp, method_getTypeEncoding(placeholderMethod));
        }
        else {
            NSLog(@"something goes wrong!");
            assert(0);
        }
        return;
    }

    if (class_addMethod(theClass, originalSel, swizzledImp, method_getTypeEncoding(swizzledMethod))) {
        // 此时(theClass 本身没有 originalSel 方法的实现,而他的某个父类有),
        // 如果有 placeholder,则和 placeholder 交换方法实现,否则和父类交换方法实现
        if (placeholderMethod && placeholderImp) {
            class_replaceMethod(theClass, swizzledSel, placeholderImp, method_getTypeEncoding(placeholderMethod));
        }
        else {
            class_replaceMethod(theClass, swizzledSel, originalImp, method_getTypeEncoding(originalMethod));
        }
    }
    else {
        method_exchangeImplementations(originalMethod, swizzledMethod);
    }
}

运行,点击button,OK!大功告成!

现在这个 swizzleInstanceMethod 基本上已经非常完善了,它引入了 placeholder 参数,让你在 method swizzling 的时候可以不受原类的对应方法是否实现的影响,极大地保证了代码的健壮性。这个 placeholder 也不是必须要传的,上例中,如果UIViewControllerdoNotSuperCallMe方法可以调用,则 placeholder 就可以传 nil;如果你非要传 placeholder,则将placeholder_doNotSuperCallMe的实现进行如下修改也是可以的:

- (void)placeholder_doNotSuperCallMe
{
    [super doNotSuperCallMe];
}

所以,这个 placeholder 是很灵活的。

最后,我再大概说一下 method swizzling 的基本原理。

当通过一个对象调用它的某个方法时,其实是先要找到这个方法的函数指针,通过这个函数指针再去找对应的实现。如果找不到这个函数指针,或者这个函数指针没有对应的实现,程序都将崩溃,当然OC也提供了一些消息转发机制保证程序继续正常运行,这个按下不表。所以 method swizzling 的基本原理总结下来就一句话:通过改变函数指针的指向来实现方法交换

现在请大家思考一个问题:假如某个类有一个方法 A,而这个类有n个分类,每个分类都有一个对应的方法 An,并且所有的方法 An 都分别和方法 A 进行了 method swizzling,请问最后这些方法的调用顺序是怎样的。

如果你理解了 method swizzling 的基本原理,这个问题应该不难。欢迎大家留言解答^_^

results matching ""

    No results matching ""